package com.unionpay.upyzt.net;

import com.google.gson.*;
import com.unionpay.upyzt.Upyzt;
import com.unionpay.upyzt.exception.*;
import com.unionpay.upyzt.resp.UpyztError;
import com.unionpay.upyzt.resp.UpyztResp;
import com.unionpay.upyzt.util.StringUtils;

import java.io.File;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.URLEncoder;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Map;
import java.util.Optional;

public abstract class APIResource extends UpyztResp {
    /**
     * URLEncoder charset
     */
    public static final Charset CHARSET = StandardCharsets.UTF_8;

    private static final HttpClient httpClient = new HttpURLConnectionClient();

    /**
     * Http request method
     */
    protected enum RequestMethod {
        GET,
        POST,
        DELETE,
        PUT
    }

    /**
     * Gson object use to transform json string to resource object
     */
    public static final Gson GSON = new GsonBuilder()
            .setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES)
            .serializeNulls()
            .disableHtmlEscaping()
            .registerTypeAdapter(LocalDate.class,
                    (JsonDeserializer<LocalDate>) (json, type, jsonDeserializationContext) ->
                            LocalDate.parse(json.getAsJsonPrimitive().getAsString(), DateTimeFormatter.ISO_OFFSET_DATE))
            .registerTypeAdapter(LocalDateTime.class,
                    (JsonDeserializer<LocalDateTime>) (json, type, jsonDeserializationContext) ->
                            LocalDateTime.parse(json.getAsJsonPrimitive().getAsString(), DateTimeFormatter.ISO_OFFSET_DATE_TIME))
            .create();

    public static Gson getGson() {
        return GSON;
    }

    public static Class<?> getSelfClass() {
        return APIResource.class;
    }

    /**
     * @param clazz the class
     * @return className
     */
    protected static String className(Class<?> clazz) {
        return StringUtils.camelToUnderline(clazz.getSimpleName()).replace("$", " ");
    }

    /**
     * @param clazz  the object class
     * @param suffix the suffix of the object
     * @return singleClassURL
     */
    protected static String singleClassURL(Class<?> clazz, String suffix) {
        return String.format("%s%s", singleClassURL(clazz), suffix);
    }

    /**
     * @param clazz the object class
     * @return singleClassURL
     */
    protected static String singleClassURL(Class<?> clazz) {
        String className = null;
        Class<?> klass = getSelfClass();
        if (!klass.getSimpleName().equalsIgnoreCase("APIResource")) {
            try {
                Method method = klass.getMethod("className", Class.class);
                className = (String) method.invoke(klass, clazz);
            } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
                e.printStackTrace();
            }
        }

        if (className == null) {
            className = className(clazz);
        }

        return String.format("%s/v1/%s", Upyzt.getApiBase(), className);
    }

    /**
     * @param clazz the object class
     * @return classURL
     */
    protected static String classURL(Class<?> clazz) {
        return String.format("%ss", singleClassURL(clazz));
    }

    /**
     * @param clazz  the object class
     * @param suffix the suffix of the object
     * @return classURL
     */
    protected static String classURL(Class<?> clazz, String suffix) {
        return String.format("%ss%s", singleClassURL(clazz), suffix);
    }

    protected static String classURLWithOutS(Class<?> clazz, String suffix) {
        return String.format("%s%s", singleClassURL(clazz), suffix);
    }

    /**
     * @param clazz the object class
     * @param id    the id of the object
     * @return instanceURL
     */
    protected static String instanceURL(Class<?> clazz, String id) {
        return String.format("%s/%s", classURL(clazz), urlEncode(id));
    }

    /**
     * @param clazz  the object class
     * @param suffix the suffix of the object
     * @param id     the id of the object
     * @return instanceURL
     */
    protected static String instanceURL(Class<?> clazz, String suffix, String id) {
        return String.format("%s%s/%s", classURL(clazz), suffix, urlEncode(id));
    }

    /**
     * @param str the string to encode
     * @return urlEncodedString
     */
    protected static String urlEncode(String str) {
        if (str == null) {
            return null;
        }

        try {
            return URLEncoder.encode(str, CHARSET.name());
        } catch (UnsupportedEncodingException e) {
            throw new AssertionError("UTF-8 is unknown");
        }
    }

    /**
     * 上传文件
     *
     * @param method  请求方法
     * @param url     请求地址
     * @param file    上传的文件
     * @param clazz   资源类
     * @param options 请求选项
     * @param <T>     资源类型
     * @return 资源
     * @throws UpyztException 异常
     */
    protected static <T> T uploadFile(
            RequestMethod method,
            String url,
            File file,
            Class<T> clazz,
            RequestOptions options) throws UpyztException {
        UpyztRequest request = new UpyztFileRequest(method, url, file, options);
        return getResource(clazz, request);
    }

    /**
     * 通过参数请求 API
     *
     * @param method  请求方法
     * @param url     请求地址
     * @param params  请求参数
     * @param clazz   资源类
     * @param options 请求选项
     * @param <T>     资源类型
     * @return 资源
     * @throws UpyztException 异常
     */
    protected static <T> T request(
            RequestMethod method,
            String url,
            Map<String, Object> params,
            Class<T> clazz,
            RequestOptions options) throws UpyztException {
        UpyztRequest request = new UpyztParamRequest(method, url, params, options);
        return getResource(clazz, request);
    }

    /**
     * 请求获取资源
     *
     * @param clazz   资源类
     * @param request 请求
     * @param <T>     资源类型
     * @return 资源
     * @throws UpyztException 异常
     */
    private static <T> T getResource(Class<T> clazz, UpyztRequest request) throws UpyztException {
        UpyztResponse response = httpClient.requestWithRetries(request);

        int responseCode = response.getResponseCode();
        String responseBody = response.getResponseBody();

        if (responseCode < 200 || responseCode >= 300) {
            handleAPIError(response);
        }

        T resource = null;
        try {
            resource = APIResource.getGson().fromJson(responseBody, clazz);
        } catch (JsonSyntaxException e) {
            raiseMalformedJsonError(responseBody, responseCode);
        }

        if (resource instanceof UpyztResp) {
            UpyztResp obj = (UpyztResp) resource;
            obj.setLastResponse(response).setRequestId(response.getResponseHeaders().firstValue("Request-Id").orElse(null));
        }

        return resource;
    }

    /**
     * 错误处理
     *
     * @param response the response
     * @throws UpyztException the upyzt exception
     */
    private static void handleAPIError(UpyztResponse response)
            throws UpyztException {
        UpyztError error = null;
        String rBody = response.getResponseBody();
        int statusCode = response.getResponseCode();
        try {
            error = APIResource.GSON.fromJson(rBody, UpyztError.class);
        } catch (JsonSyntaxException e) {
            raiseMalformedJsonError(rBody, statusCode);
        }
        Optional<String> optionalReqId = response.getResponseHeaders().firstValue("Request-Id");
        String reqId = optionalReqId.orElse(null);
        switch (statusCode) {
            case 400:
            case 404:
                throw new InvalidRequestException(error.getMessage(), error.getCode(), error.getField(), error.getIssue(), statusCode, reqId);
            case 403:
                throw new PermissionException(error.getMessage(), error.getCode(), statusCode, reqId);
            case 429:
                throw new RateLimitException(error.getMessage(), error.getCode(), statusCode, reqId);
            case 401:
                throw new AuthenticationException(error.getMessage(), error.getCode(), statusCode, reqId);
            case 502:
                throw new APIConnectionException(error.getMessage(), error.getCode(), statusCode, reqId);
            default:
                throw new APIException(error.getMessage(), error.getCode(), statusCode, reqId);
        }
    }

    private static void raiseMalformedJsonError(
            String responseBody, int responseCode) throws APIException {
        throw new APIException(
                String.format(
                        "Invalid response object from API: %s. (HTTP response code was %d)",
                        responseBody, responseCode),
                null,
                responseCode);
    }
}
